package liquidfs.ui.net;

import java.io.*;
import java.net.*;
import java.util.*;
import java.util.logging.*;

import liquidfs.*;
import liquidfs.core.*;
import liquidfs.data.*;
import liquidfs.data.ops.*;

import utility.*;

public class MessageClient {

   private Application app;
   private Context context;
   
	private Socket xmlSocket;
	private Socket binSocket;
	
	private InputStream xmlIn;
	private OutputStream xmlOut;
	
	private InputStream binIn;
	private OutputStream binOut;
	
	private Serializer serializer;
	
	private byte[] buffer = new byte[1024];
	
	private Logger log = Logger.getLogger("liquidfs.ui.net");
	
	public MessageClient (Application app, Host host) throws IOException {
	   
	   this.app = app;
	   this.context = app.getContext();
	   this.serializer = Serializer.getDefaultSerializer(app);
	   
		try {
			this.xmlSocket = new Socket(host.getName(), host.getPort());
		}
		catch (IOException e) {
			throw new IOException("Unable to connect to " + host.toString() + ";\n" + e.getLocalizedMessage(), e); 
		}
		
		try {
			this.binSocket = new Socket(host.getName(), host.getPort() + 1);
		}
		catch (IOException e) {
			throw new IOException("Unable to connect to " + host.getName() + ":" + (host.getPort() + 1) + ";\n" + e.getLocalizedMessage(), e); 
		}
		
		// Open the client's data streams
		try	{
			xmlIn = xmlSocket.getInputStream();
			xmlOut = xmlSocket.getOutputStream();
			
			binIn = new DataInputStream(binSocket.getInputStream());
			binOut = new DataOutputStream(binSocket.getOutputStream());
		}
		catch (IOException e) {
		   try {
		      cleanup ();
		   }
		   catch (IOException f) {
		      // Do nothing
		   }
		   
			throw new IOException("Failed to open the data streams of the client at " + this.toString() + ";\n" + e.getLocalizedMessage(), e);
		}
	}
	
	MessageClient (Application app, Socket xmlSocket, Socket binSocket) throws IOException {
		
	   this.app = app;
	   this.context = app.getContext();
	   this.serializer = Serializer.getDefaultSerializer(app);
	   
		this.xmlSocket = xmlSocket;
		this.binSocket = binSocket;
		
		byte[] address1 = this.xmlSocket.getInetAddress().getAddress();
		byte[] address2 = this.binSocket.getInetAddress().getAddress();
		
		boolean inequal = false;
		for (int i = 0; i < address1.length && i < address2.length; ++i) 
		{	
			if (address1[i] != address2[i]) {
				inequal = true;
				break;
			}
		}
		
		if (inequal || address1.length != address2.length) {
			cleanup ();
			throw new IOException("XML and binary data streams do not originate from the same IP address.");
		}
		
		// Open the client's data streams
		try	{
			xmlIn = xmlSocket.getInputStream();
			xmlOut = xmlSocket.getOutputStream();
			
			binIn = new DataInputStream(binSocket.getInputStream());
			binOut = new DataOutputStream(binSocket.getOutputStream());
		}
		catch (IOException e) {
			cleanup ();
			throw new IOException("Failed to open the data streams of the client at " + this.toString() + ";\n" + e.getLocalizedMessage(), e);
		}
	}
	
	private void pipe (InputStream in, OutputStream out) throws IOException {

	   // If both of the streams are valid
      if (in != null && out != null) {
         
         try {
         
            int read;
            do {
               
               // Read from the input and write to the output
               read = in.read(buffer, 0, buffer.length);
               
               if (read == -1)
                  return;
               
               out.write(buffer, 0, read);
               
            }
            while (read != -1);
         }
         catch (EOFException e)
         {
            // That's fine, that just means we're done.
            return;
         }
         catch (IOException e) 
         {
            this.cleanup();
         }
      }
	}
	
	public <T> T receive () throws IOException {
      
      return this.receive(null);
   }
	
	public <T> T receive (OutputStream out) throws IOException {
      
      this.pipe(this.binIn, out);
            
      // Receive the message
      @SuppressWarnings("unchecked")
      T message = (T) serializer.deserialize(xmlIn);
      
      // Make sure it's valid
      if (message == null) {
         
         try {
            cleanup();
         }
         catch (IOException e) {
            // Do nothing
         }
         
         throw new IOException("The message from client " + this.toString() + " was not received properly.");
      }
      
      // Return it to the user
      return message;
   }
	
	public <T> void send (T message) throws IOException {
	   this.send(message, null);
	}
	
	public <T> void send(T message, InputStream in) throws IOException {
	   
	   log.finer(this.app.getConfiguration().getLocalDevice().getID() + " sending message to " + this.toString() + "\n" + serializer.serialize(message) + "\n\n");
      serializer.serialize(message, xmlOut);
      
      this.pipe(in, this.binOut);
   }
	
	public InputStream getBinaryInput () {
		return this.binIn;
	}
	
	public OutputStream getBinaryOutput () {
		return this.binOut;
	}
	
	public void close () throws IOException {
	   this.cleanup();
	}
	
	private void cleanup () throws IOException {
	   
	   List<IOException> exceptions = new ArrayList<IOException>();
	   
	   try {
	      xmlIn.close();
	   }
	   catch (IOException e) {
	      exceptions.add(e);
	   }
	   
	   try {
         xmlOut.close();
      }
      catch (IOException e) {
         exceptions.add(e);
      }
	   
	   try {
         binIn.close();
      }
      catch (IOException e) {
         exceptions.add(e);
      }
	   
	   try {
         binOut.close();
      }
      catch (IOException e) {
         exceptions.add(e);
      }
	   
		try {
			if (xmlSocket != null)
				xmlSocket.close();
		}
		catch (IOException e) {
		   exceptions.add(e);
		}
		
		try {
			if (binSocket != null)
				binSocket.close();
		}
		catch (IOException e) {
		   exceptions.add(e);
		}
		
		if (exceptions.size() == 1) {
		   throw exceptions.get(0);
		}
		else if (exceptions.size() > 1) {
		   throw new IOException("Did not successfully close streams; data may not have been sent.");
		}
	
	}
	
	public String toString ()
	{
		return xmlSocket.getInetAddress().toString() + ":" + xmlSocket.getPort() + "/" + binSocket.getPort();
	}	
}
